JavaScriptCore 解析

JavaScript 越来越多的出现在客户端的开发当中,ReactNative、JSPatch 都是利用 JavaScript 与客户端语言结合实现的。在 iOS 中,使 iOS 拥有执行 JavaScript 代码能力的的便是 JavaScriptCore 框架。

JavaScriptCore 简介

JavaScriptCore 是苹果 Safari 浏览器的 JavaScript 引擎,是专门处理 JavaScript 脚本的虚拟机。当前主要的还在开发中的 JavaScript 引擎如下

img

JavaScriptCore框架 是苹果在 iOS7 引入的框架,使用该框架可以在 Objective-C 或者基于C的程序中执行 Javascript 代码,也可以向 JavaScript 环境中插入一些自定义的对象。

JavaScriptCore框架 其实就是基于 webkit 中以C/C++实现的 JavaScriptCore 的一个包装,在旧版本iOS开发中,很多开发者也会自行将 webkit 的库引入项目编译使用。现在iOS7把它当成了标准库。

JavaScriptCore 解析

查看 JavaScriptCore.h,可以看到 JavaScriptCore 的几个主要类或协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef JavaScriptCore_h
#define JavaScriptCore_h

#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>

#if defined(__OBJC__) && JSC_OBJC_API_ENABLED

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

#endif

#endif /* JavaScriptCore_h */
  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

JSVirtualMachine

一个 JSVirtualMachine 的实例就是一个完整独立的 JavaScript 的执行环境,为 JavaScript 的执行提供底层资源,它有自己独立的堆栈以及垃圾回收机制

这个类主要做两件事情:

  1. 实现并发的 JavaScript 执行
  2. JavaScript 和 Objective-C 桥接对象的内存管理
1
2
3
4
5
6
7
8
9
NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject
/* 创建一个新的完全独立的虚拟机 */
- (instancetype)init;
/* 对桥接对象进行内存管理 */
- (void)addManagedReference:(id)object withOwner:(id)owner;
/* 取消对桥接对象进行内存管理 */
- (void)removeManagedReference:(id)object withOwner:(id)owner;
@end

每一个JavaScript上下文(JSContext对象)都归属一个虚拟机(JSVirtualMachine对象)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。

每个虚拟机都是独立的,有其自己的堆栈以及垃圾回收机制(garbage collector)。GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

img

JavaScriptCore API 都是线程安全的,是通过锁定虚拟机实现的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。

  • 如果想并发执行JS,需要使用多个不同的虚拟机来实现。
  • 可以在子线程中执行JS代码。

JSContext

一个 JSContext 对象代表一个JavaScript 执行环境。在native代码中,使用 JSContext 去执行JS代码,访问JS中定义或者计算的值,并使 JavaScript 可以访问native的对象、方法、函数。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSContext : NSObject

/*!
@methodgroup Creating New JSContexts
*/

/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */
- (instancetype)init;
/* 在指定虚拟机上创建一个JSContext */
- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;

/*!
@methodgroup Evaluating Scripts
*/

/* 执行一段JS代码,返回最后生成的一个值 */
- (JSValue *)evaluateScript:(NSString *)script;
/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL NS_AVAILABLE(10_10, 8_0);

/*!
@methodgroup Callback Accessors
*/

/* 获取当前执行的JavaScript代码的context */
+ (JSContext *)currentContext;
/* 获取当前执行的JavaScript function*/
+ (JSValue *)currentCallee NS_AVAILABLE(10_10, 8_0);
/* 获取当前执行的JavaScript代码的 <code>this</code> */
+ (JSValue *)currentThis;
/* 获取当前执行的JavaScript代码回调的参数 */
+ (NSArray *)currentArguments;

/*!
@functiongroup Global Properties
*/

/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/
@property (readonly, strong) JSValue *globalObject;

/* 当执行一个未定义的 JS 语句时,context会抛出异常。如果 exceptionHandler Block 不为 nil,则回调 block。否则,将异常静默保存到 exception 属性中*/
@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);

/* 获取与当前 context 关联的虚拟机 */
@property (readonly, strong) JSVirtualMachine *virtualMachine;

@property (copy) NSString *name NS_AVAILABLE(10_10, 8_0);

@end
  1. JSContext 执行 JS 代码

    • 调用evaluateScript函数可以执行一段top-level 的JS代码,并可向 global 对象添加函数和对象定义
    • 其返回值是 JavaScript 代码中最后一个生成的值
  2. JSContext访问JS对象

一个 JSContext 对象对应了一个全局对象(global object)。例如web浏览器中的 JSContext,其全局对象就是window 对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域,实际上JS代码都是在这个 GlobalObject 上执行的,但是为了容易理解,可以把 JSContext 等价于全局对象。

JavaScript 中的任何一个全局函数或变量都是 window 的属性。可以通过JSValue对象或者context下标的方式来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取上下文
JSContext *context = [[JSContext alloc] init];
// 执行JavaScript代码并获取返回值
[context evaluateScript:@"var a = 1 + 2;"];
// 转换OC数据并打印
NSLog(@"%@", [context objectForKeyedSubscript:@"a"]);
NSLog(@"%@", [context.globalObject objectForKeyedSubscript:@"a"]);
NSLog(@"%@", context[@"a"]);
NSLog(@"%@", context.globalObject[@"a"]);

// 输出结果
3
3
3
3

这里有三那种访问 JavaScript 对象的方法:

  • 通过 context 实例对象的 objectForKeyedSubscript:方法访问
  • 通过 context.globalObject(JSValue类型)对象的objectForKeyedSubscript:方法访问
  • 通过下标

设置属性也是对应的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 为JSContext提供下标访问元素的方式 */
@interface JSContext (SubscriptSupport)

/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */
- (JSValue *)objectForKeyedSubscript:(id)key;

/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */
- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;

@end

/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */
context[@"makeColor"] = ^(NSDictionary *rgb){
CGFloat red = [rgb[@"red"] floatValue];
CGFloat green = [rgb[@"green"] floatValue];
CGFloat blue = [rgb[@"blue"] floatValue];
return [UIColor colorWithRed:red/255.0 green:green/255.0 blue:blue/255.0 alpha:1];
};
// 执行JavaScript代码并获取返回值
JSValue *value = [context evaluateScript:@"makeColor({red:23, green:45, blue:89})"];
// 转换OC数据并打印
NSLog(@"%@", [value toObject]);

// 输出结果
UIExtendedSRGBColorSpace 0.0901961 0.176471 0.34902 1

JSValue

一个 JSValue 实例就是一个JavaScript值的引用。是对 JS 值得包装,例如 JS 的 number,boolean等基本类型,也可以是对象,函数。对 JS 和 OC 应类型如下:

img

JSValue 是不能独立存在的,它必须存在于某一个 JSContext 中,一个 JSContext 中可以包含多个 JSValue。JSValue 对其对应的 JS 值和其所属的 JSContext 对象都是强引用的关系,只要有任何一个 JSValue 被持有(retain),对应的 JSContext就不会被销毁。

每个 JSValue 都通过其 JSContext 间接关联了一个特定的 JSVirtualMachine 对象。你只能将一个 JSValue 对象传给由相同虚拟机管理(host)的 JSValue 或者 JSContext 的实例方法。如果尝试把一个虚拟机的 JSValue 传给另一个虚拟机,将会触发一个Objective-C异常。
img

NSDictionary,NSArray 与 JS 对象

NSDictionary 对象以及其包含的 keys 与 JavaScript 中的对应名称的 Object类型对象 相互转换。

JS 中的对象可以直接转换成 Objective-C 中的 NSDictionary,NSDictionary传入 JavaScript 也可以直接当作对象被使用

NSArray 对象也是类似的转换方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    [context evaluateScript:@"var color = {red:255, green:45, blue:89}"];
JSValue *value = context[@"color"];
NSLog(@"%@", [value toObject]);

context[@"color"] = @{@"red":@33, @"green":@100, @"blue":@255};
value = context[@"color"];
NSLog(@"%@", [value toObject]);

// 输出结果
{
blue = 89;
green = 45;
red = 255;
}
{
blue = 255;
green = 100;
red = 33;
}

Block/函数 和 JS function

Objective-C中的block转换成 JavaScript 中的 function 对象。参数以及返回类型使用相同的规则转换。

将一个代表 native 的block或者方法的 JavaScript function 进行转换将会得到那个 block 或方法。

其他的 JavaScript 函数将会被转换为一个空的 dictionary。因为 JavaScript 函数也是一个对象。

OC 对象和 JS 对象

对于所有其他 native 的对象类型,JavaScriptCore 都会创建一个拥有 constructor 原型链的 wrapper 对象,用来反映native类型的继承关系。默认情况下,native对象的属性和方法并不会导出给其对应的 JavaScript wrapper 对象。通过 JSExport 协议可选择性地导出属性和方法。

JSExport

JSExport 协议提供了一种声明式的方法去向 JavaScript 代码导出 Objective-C 的实例类及其实例方法,类方法和属性。

在 JavaScript 中调用 OC 代码

有两种方式:

  • Block
  • JSExport 协议

Block 的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JSContext *context = [[JSContext alloc] init];
context[@"add"] = ^(NSInteger a, NSInteger b){
return a + b;
};

// 一种调用 JS 函数的方法,直接通过 evaluateScript 调用
JSValue *result = [context evaluateScript:@"add(3, 4)"];
NSLog(@"%@", [result toNumber]);

// 另一种调用 JS 函数的方法,先获取对应的函数对象,在使用 callWithArguments 调用,并传入参数
JSValue *value = context[@"add"];
JSValue *result = [value callWithArguments:@[@7, @8]];
NSLog(@"%@", [result toNumber]);

// 输出结果
7
15

JSExport 的方式,需要通过继承 JSExport 协议的方式来导出指定的方法和属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 声明一个继承自 JSExport 的协议,在其中声明可以被 JS 导出的方法和属性
@protocol GGJSExport<JSExport>
@property (nonatomic, assign) CGFloat x;
@property (nonatomic, assign) CGFloat y;
- (CGFloat)add;
+ (instancetype)JSObjectWithX:(CGFloat)x y:(CGFloat)y;
@end

// 声明一个类,遵守 GGJSExport 协议,实现协议中的方法和属性
// .h
@interface GGJSObject : NSObject<GGJSExport>
@property (nonatomic, assign) CGFloat x;
@property (nonatomic, assign) CGFloat y;

// 未在协议中声明的方法和属性,JS 无法识别
@property (nonatomic, copy) NSString *name;
- (void)doSomething;
@end

// .m
@implementation GGJSObject

- (CGFloat)add {
return self.x + self.y;
}

+ (instancetype)JSObjectWithX:(CGFloat)x y:(CGFloat)y {
GGJSObject *obj = [[GGJSObject alloc] init];
obj.x = x;
obj.y = y;
return obj;
}

- (void)doSomething {
NSLog(@"%@---%@", @(self.x), @(self.y));
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
JSContext *context = [[JSContext alloc] init];
// 添加异常回调,在 js 调用异常时会回调次 block
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"%@--------%@", context, exception);
};

// 将实例对象和类导出给 context
context[@"jsObject"] = [[GGJSObject alloc] init];
context[@"GGJSObject"] = [GGJSObject class];
// 属性赋值
[context evaluateScript:@"jsObject.x = 100"];
NSLog(@"x:%@", context[@"jsObject"][@"x"]);

// 调用实例方法
JSValue *result = [context evaluateScript:@"jsObject.add()"];
NSLog(@"result:%@", result);

// 调用类方法
JSValue *classResult = [context evaluateScript:@"GGJSObject.JSObjectWithXY(20, 30)"];
NSLog(@"obj:%@-----x:%@--------y:%@", [classResult toObject], classResult[@"x"], classResult[@"y"]);

// 调用协议没有声明的方法,回触发异常,显示此方法未定义
[context evaluateScript:@"jsObject.doSomething()"];

// 输出
x:100
result:100
obj:<GGJSObject: 0x604000034280>-----x:20--------y:30
<JSContext: 0x6040000486d0>--------TypeError: jsObject.doSomething is not a function. (In 'jsObject.doSomething()', 'jsObject.doSomething' is undefined)
  • 继承自 JSExport 的协议中,声明的类方法,实例方法和属性(如:x-add+JSObjectWithX: y:),都可以背 JS 获取到并且调用。未在协议中声明的方法或属性(如:-doSomething),则无法被 JS 获取到。
  • 对于每一个导出的实例方法,JavaScriptCore 都会在 prototype 中创建一个存取器属性。对于每一个导出的类方法,JavaScriptCore会在 constructor 对象中创建一个对应的 JavaScript function。
  • 在Objective-C中通过@property声明的属性决定了JavaScript中的对应属性的特征:1492485215152_2649_1492485215481
  • Objective-C类中的属性,成员变量以及返回值都将根据JSValue指定的拷贝协议进行转换。

函数名转换

转换成驼峰形式:

  • 去掉所有的冒号
  • 所有冒号后的第一个小写字母都会被转为大写
1
2
3
4
// oc 方法
JSObjectWithX: y:
// 对应 JS function
JSObjectWithXY(x, y)

如果不喜欢默认的转换规则,也可以使用 JSExportAs 进行自定义转换

1
2
3
4
5
6
7
8
9
10
11
12
13
// JSExportAs 的宏定义(JSExport.h)
#define JSExportAs(PropertyName, Selector) \
@optional Selector __JS_EXPORT_AS__##PropertyName:(id)argument; @required Selector
#endif

// 在协议中自定义声明
@protocol GGJSExport<JSExport>
JSExportAs(JSObject, + (instancetype)JSObjectWithX:(CGFloat)x y:(CGFloat)y);
//+ (instancetype)JSObjectWithX:(CGFloat)x y:(CGFloat)y;
@end

// 调用
[context evaluateScript:@"GGJSObject.JSObject(20, 30)"];

实现原理

当声明一个继承自 JSExport 的自定义协议时,就是在告诉 JSCore,这个自定义协议中声明的属性,实例方法和类方法需要被暴露给JS使用。(不在这个协议中的方法不会被暴露出去。)

当把实现这个协议的类的对象暴露给 JS 时,JS 中会生成一个对应的 JS 对象,然后,JSCore会按照这个协议中声明的内容,去遍历实现这个协议的类,把协议中声明的属性,转换成 JS 对象中的属性,实质上是转换成 getter 和 setter 方法,转换方法和之前说的block类似,创建一个JS方法包装着OC中的方法,然后协议中声明的实例方法,转换成 JS 对象上的实例方法,类方法转换成 JS 中某个全局对象上的方法。

img

JSManagedValue

循环引用

由于每个 JSValue 对其对应的 JS 值和其所属的 JSContext 对象都是强引用的关系,只要有任何一个 JSValue 被持有(retain),对应的 JSContext就不会被销毁。如果我们将一个 native 对象导出给 JavaScript,即将这个对象交由JavaScript 的全局对象持有,引用关系如下:

1492486033041_8675_1492486033267

如果我们在 native 对象(如Block)中强引用持有 JSContext 或者 JSValue,便会造成循环引用:

1492486061808_9327_1492486062018

因此在使用时要注意:

  • 避免直接使用外部context

    • 避免在导出的 block/native 函数中直接使用 JSContext
    • 使用 [JSContext currentContext] 来获取当前context能够避免循环引用
1
2
3
4
5
6
7
8
9
// 错误用法
context[@"block"] = ^() {
NSLog(@"%@", context);
};

// 纠正用法
context[@"block"] = ^() {
NSLog(@"%@", [JSContext currentContext]);
};
  • 避免直接使用外部JSValue

    • 避免在导出的 block/native 函数中直接使用 JSValue
1
2
3
4
5
6
7
8
9
10
11
12
// 错误用法
JSValue *value = [JSValue valueWithObject:@"test" inContext:context];
context[@"block"] = ^(){
NSLog(@"%@", value);
};

// 纠正用法,使用 JSManagedValue 来解决问题
JSValue *value = [JSValue valueWithObject:@"test" inContext:context];
JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:value andOwner:self];
context[@"block"] = ^(){
NSLog(@"%@", [managedValue value]);
};

JSManagedValue

一个JSManagedValue对象包含了一个JSValue对象,“有条件地持有(conditional retain)”的特性使其可以自动管理内存。

所谓“有条件地持有(conditional retain)”,是指在以下两种情况任何一个满足的情况下保证其管理的 JSValue 被持有:

  • 可以通过 JavaScript 的对象图找到该 JSValue

  • 可以通过 native 对象能找到该 JSManagedValue。

使用 addManagedReference:withOwner: 方法可向虚拟机记录该关系。反之,如果以上条件都不满足,JSManagedValue 对象就会将其 value 置为 nil 并释放该 JSValue。JSManagedValue 对其包含的 JSValue 的持有关系与ARC下的虚引用(weak reference)类似。

通常我们使用 weak 来修饰 block 内需要使用的外部引用以避免循环引用,由于 JSValue 对应的JS对象内存由虚拟机进行管理并负责回收,这种方法不能准确地控制block内的引用JSValue的生命周期,可能在block内需要使用JSValue的时候,其已经被虚拟机回收。所以做好不使用 weak 引用,而是使用 JSManagedValue。

1
2
3
4
5
6
// 可以使用 JSVirtualMachine 手动管理
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;

// 也可以使用 JSManagedValue 的类方法直接产生一个带owner的对象
+ (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:(id)owner;

参考

JavaScriptCore全面解析 (上篇)

JavaScriptCore全面解析(下篇)

深入浅出 JavaScriptCore

Integrating JavaScript into Native Apps